﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Net;
using System.Text.RegularExpressions;
using System.Text;

/// 
/// Contains SPF Mechanism base class and all mechanism derived class
/// 

namespace InterPC.SPF.SPFMechanisms {

    /// <summary>
    /// Base class for all mechanisms
    /// </summary>
    public class SPFMechanismTerm : SPFTerm {
        protected SPFMechanismQualifier _Qualifier;
        /// <summary>
        /// Qualifier of the mechanism
        /// </summary>
        public SPFMechanismQualifier Qualifier {
            get { return _Qualifier; }
        }
        protected SPFMechanismType _MechanismType;
        /// <summary>
        /// Type of the mechanism
        /// </summary>
        public SPFMechanismType MechanismType {
            get { return _MechanismType; }
        }
        internal virtual string _Arg { get; set; }
        /// <summary>
        /// Arg of the mechanism if any
        /// </summary>
        public string Arg {
            get { return _Arg; }
        }
        private int _IP4Cidr = -1;
        /// <summary>
        /// IP4 Cidr to apply to arg
        /// </summary>
        public int IP4Cidr {
            get { return _IP4Cidr; }
            set { _IP4Cidr = value; }
        }
        private int _IP6Cidr = -1;
        /// <summary>
        /// IP6 Cidr to apply to arg
        /// </summary>
        public int IP6Cidr {
            get { return _IP6Cidr; }
            set { _IP6Cidr = value; }
        }


        /// <summary>
        /// Main constructor
        /// </summary>
        public SPFMechanismTerm() : base(SPFTermType.Mechanism) { }

        /// <summary>
        /// Parse a string to a mechanism object
        /// Throw an exception if the format is invalid
        /// </summary>
        /// <param name="datas">string to parse</param>
        /// <returns>the parsed SPFMechanismTerm</returns>
        public new static SPFMechanismTerm Parse(string datas) {

            Regex MechReg = new Regex(@"^(?<qualifier>[+?~-]?)(?<mechanism>(all)|(include)|(A)|(MX)|(PTR)|(IP4)|(IP6)|(exists))(:?(?<argument>.+))?$", RegexOptions.IgnoreCase);
            Match M = MechReg.Match(datas);
            if (!M.Success)
                throw new FormatException(string.Format("Mechanism format is incorrect ({0})", datas));
            
            Dictionary<char, SPFMechanismQualifier> QualifierConversion = new Dictionary<char, SPFMechanismQualifier>() {
                    { '-', SPFMechanismQualifier.Fail },
                    { '~', SPFMechanismQualifier.SoftFail },
                    { '?', SPFMechanismQualifier.Neutral },
                    { '+', SPFMechanismQualifier.Pass }
                };

            SPFMechanismType MType = SPFMechanismType.All;
            SPFMechanismQualifier MQualifier = SPFMechanismQualifier.Pass;
            string MArg = null;
            int MIP4Cidr = -1;
            int MIP6Cidr = -1;
            try {
                MType = (SPFMechanismType)Enum.Parse(typeof(SPFMechanismType), M.Groups["mechanism"].Value, true);
                MQualifier =
                    M.Groups["qualifier"].Value != "" ?
                    QualifierConversion[M.Groups["qualifier"].Value[0]]
                    : SPFMechanismQualifier.Pass;
                MArg = M.Groups["argument"].Value;
                if (MArg == ":")
                    throw new ArgumentException("Domain spec cannot be empty");
                
                // Cidr
                Regex CidrReg = new Regex(@"/(?<cidr1>[0-9]+)?/?(/(?<cidr2>[0-9]+))?$");
                Match CidrM = CidrReg.Match(MArg); 
                if (CidrM.Success) {
                    // Leading 0 in Cidr are prohibited
                    if (
                        Regex.IsMatch(CidrM.Groups["cidr1"].Value, "^0[0-9]+$") ||
                        Regex.IsMatch(CidrM.Groups["cidr2"].Value, "^0[0-9]+$")
                        )
                        throw new ArgumentException("Invalid cidr length format");
                    // Remove cidr from arg
                    MArg = CidrReg.Replace(MArg, "");
                    bool Has2Cidr = CidrM.Value.Contains("//");
                    switch (MType) {
                        case SPFMechanismType.A:
                        case SPFMechanismType.MX:
                            if (Has2Cidr) {
                                MIP4Cidr = string.IsNullOrEmpty(CidrM.Groups["cidr1"].Value) ? -1 : int.Parse(M.Groups["cidr1"].Value);
                                MIP6Cidr = int.Parse(CidrM.Groups["cidr2"].Value);
                            }
                            else
                                MIP4Cidr = int.Parse(CidrM.Value.Trim('/'));
                            break;
                        case SPFMechanismType.IP4:
                            if (Has2Cidr)
                                throw new ArgumentException("IP4 mechanism cannot have dual-cidr-length");
                            else
                                MIP4Cidr = int.Parse(CidrM.Value.Trim('/'));
                            break;
                        case SPFMechanismType.IP6:
                            if (Has2Cidr)
                                throw new ArgumentException("IP6 mechanism cannot have dual-cidr-length");
                            else
                                MIP6Cidr = int.Parse(CidrM.Value.Trim('/'));
                            break;
                        default:
                            throw new ArgumentException(string.Format("{0} mechanism cannot have cidr-length", MType));
                    }
                }
            }
            catch {
                throw new FormatException(string.Format("Mechanism format is incorrect ({0})", datas));
            }

            SPFMechanismTerm Result = null;
            switch (MType) {
                case SPFMechanismType.A:
                    Result = new SPFMechanisms.A();
                    break;
                case SPFMechanismType.All:
                    Result = new SPFMechanisms.All();
                    break;
                case SPFMechanismType.Exists:
                    Result = new SPFMechanisms.Exists();
                    break;
                case SPFMechanismType.Include:
                    Result = new SPFMechanisms.Include();
                    break;
                case SPFMechanismType.IP4:
                    //  Basic verify IP Format (short format not allowed, cidr must be used)
                    if (!Regex.IsMatch(MArg, @"^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$"))
                        throw new ArgumentException("IP4 address format is invalid");
                    Result = new SPFMechanisms.IP4();
                    break;
                case SPFMechanismType.IP6:
                    Result = new SPFMechanisms.IP6();
                    break;
                case SPFMechanismType.MX:
                    Result = new SPFMechanisms.MX();
                    break;
                case SPFMechanismType.PTR:
                    Result = new SPFMechanisms.PTR();
                    break;

            }

            Result._IP4Cidr = MIP4Cidr;
            Result._IP6Cidr = MIP6Cidr;

            // Ensure cidr range
            if (Result.IP4Cidr > 32)
                throw new ArgumentException("IPv4 Cidr cannot be higher than 32");
            if (Result.IP6Cidr > 128)
                throw new ArgumentException("IPv6 Cidr cannot be higher than 32");

            Result._Arg = MArg;
            Result._MechanismType = MType;
            Result._Qualifier = MQualifier;
            if (Result.IP4Cidr == -1)
                Result.IP4Cidr = 32;
            if (Result.IP6Cidr == -1)
                Result.IP6Cidr = 128;

            return Result;
        }

        /// <summary>
        /// Returns a normalized string representation of the mechanism
        /// </summary>
        public override string ToString() {
            Dictionary<SPFMechanismQualifier, char> QualifierConversion = new Dictionary<SPFMechanismQualifier, char>() {
                    { SPFMechanismQualifier.Fail, '-' },
                    { SPFMechanismQualifier.SoftFail, '~' },
                    { SPFMechanismQualifier.Neutral, '?' },
                    { SPFMechanismQualifier.Pass, '+' }
                };
            StringBuilder Sb = new StringBuilder(
                string.Format("{0}{1}{2}",
                QualifierConversion[_Qualifier],
                _MechanismType,
                string.IsNullOrEmpty(_Arg) ? "" : ":" + _Arg
                )
             );
            if (IP4Cidr != 32 && this.MechanismType != SPFMechanismType.IP6)
                Sb.AppendFormat("/{0}", IP4Cidr);
            if (IP6Cidr != 128 && this.MechanismType != SPFMechanismType.IP4) {
                if (this.MechanismType != SPFMechanismType.IP6)
                    Sb.Append("/");
                Sb.AppendFormat("/{0}", IP6Cidr);
            }
            return Sb.ToString();
        }

        /// <summary>
        /// Check the mechanism regarding to an ip adress
        /// </summary>
        /// <param name="_Parent">Parent checker object for recursion purpose</param>
        /// <param name="ip">IPadress to check</param>
        /// <param name="domain">Domain currently checked</param>
        /// <returns>Qualifier resulting from the check, regarding (correspond to the current mechanism qualifier or neutral)</returns>
        public virtual SPFMechanismQualifier Check(SPFChecker _Parent, IPAddress ip, string domain) { return SPFMechanismQualifier.Neutral; }

        /// <summary>
        /// Provied a quick method to retrieve A records from a hostname
        /// </summary>
        /// <param name="HostName">Host for wich to retrieve the address</param>
        /// <param name="max">Maximum adresses to retrieve</param>
        /// <returns></returns>
        protected IPAddress[] GetAddressesForHostName(string HostName, int max, bool IP6) {
            try {
                IPAddress[] ips =
                    IP6 ?
                    SPFChecker.DnsProxy.GetAAAA(HostName) :
                    SPFChecker.DnsProxy.GetA(HostName);
                if (ips.Length > max)
                    ips = ips.Take(max).ToArray();
                return ips;
            }
            catch (SocketException ex) {
                HandleSocketException(ex);
                return new IPAddress[0];
            }
        }

        /// <summary>
        /// Test ip with DNS records obtained from DomainToQuery and for a specified type of record
        /// Returns MatchResult if match found or Neutral if not
        /// Ex : test 1.2.3.4 with all MX records of foo.fr
        ///      test 4.5.6.7 with all A records of msbill.com
        /// </summary>
        /// <param name="ip">IP to test</param>
        /// <param name="DomainToQuery">Domain to query DNS records from</param>
        /// <param name="MatchResult">Result to return if match</param>
        /// <param name="RecordType">Type of record to query from DomainToQuery</param>
        /// <returns>The resulting qualifier</returns>
        protected SPFMechanismQualifier CheckFromRecord(IPAddress ip, int cidr, string domainToQuery, SPFMechanismQualifier matchResult, Heijden.DNS.Type recordType) {

            // Get records of type RecordType from DomainToQuery
            object result = null;
            try {
                result = SPFChecker.DnsProxy.Lookup(domainToQuery, (ushort)recordType);
            }
            catch (SocketException ex) { return HandleSocketException(ex); }

            // Foreach record, get the list of IPAddress to compare with ip
            // A (IP4) / AAA (IP6) -> address from the record
            // MX -> resolved adresses of the MTA
            List<IPAddress> AddressesToCompare = new List<IPAddress>();
            foreach (object Record in (Array)result) {
                switch (recordType) {
                    case Heijden.DNS.Type.A:
                        // convert DNS_A_DATA to IPAddress and add
                        AddressesToCompare.Add((IPAddress)Record);
                        break;
                    case Heijden.DNS.Type.AAAA:
                        // convert DNS_IP6_ADDRESS to IPAddress and add
                        AddressesToCompare.Add((IPAddress)Record);
                        break;
                    case Heijden.DNS.Type.MX:
                        // Resolve address of the mail exchanger and add
                        AddressesToCompare.AddRange(GetAddressesForHostName(((MXRecord)Record).Exchange, 10, ip.AddressFamily == AddressFamily.InterNetworkV6));
                        break;
                    default:
                        return SPFMechanismQualifier.Neutral;
                }
            }
            // If no address has been found, return neutral
            if (AddressesToCompare.Count == 0)
                return SPFMechanismQualifier.Neutral;

            // Test each found ip with the given ip
            // BitArray for tested ip
            BitArrayEx TestedBits = new BitArrayEx(ip.GetAddressBytes());
            // If cidr > 0, apply mask
            TestedBits.ApplyMask(cidr);
            // foreach found ip
            foreach (IPAddress IPToCompare in AddressesToCompare) {
                // BitArray for the ip
                BitArrayEx BitsToCompare = new BitArrayEx(IPToCompare.GetAddressBytes());
                // If cidr > 0, apply mask
                BitsToCompare.ApplyMask(cidr);
                // Compare with given ip bitarray, return MatchResult if success
                if (TestedBits == BitsToCompare)
                    return matchResult;
            }

            // No match found, return neutral
            return SPFMechanismQualifier.Neutral;

        }

        internal static SPFMechanismQualifier HandleSocketException(SocketException ex) {

            // DNS_ERROR_RCODE_NAME_ERROR (domain does not exists)
            if (ex.ErrorCode == 9003) 
                return SPFMechanismQualifier.Neutral;
                
            // All other Dns error
            throw new TempErrorException(string.Format("Dns error : {0}", ex.Message));
        }
    }

    public class All : SPFMechanismTerm {

        internal override string _Arg {
            get {
                return base._Arg;
            }
            set {
                if (!string.IsNullOrEmpty(value))
                    throw new FormatException("All mechanism accept no args");
            }
        }

        public All() : base() { this._MechanismType = SPFMechanismType.All; }

        public override SPFMechanismQualifier Check(SPFChecker _Parent, System.Net.IPAddress ip, string domain) {
            // Simply return the qualifier
            return this._Qualifier;
        }

    }

    public class Include : SPFMechanismTerm {

        public Include() : base() { this._MechanismType = SPFMechanismType.Include; }

        public override SPFMechanismQualifier Check(SPFChecker _Parent, System.Net.IPAddress ip, string domain) {
            
            // Arg cannot be null
            if (string.IsNullOrEmpty(this._Arg))
                throw new PermErrorException("Include argument cannot be empty");

            // Prévent infinite recursion error
            string cleanDomain = this._Arg.ToLower().Trim();
            if (_Parent._CheckedDomains.ContainsKey(cleanDomain)) {
                // If result set, return the result
                if (_Parent._CheckedDomains[cleanDomain].HasValue)
                    return _Parent._CheckedDomains[cleanDomain].Value;
                // If not set : Inifite loop started !!
                else
                    throw new PermErrorException("Infinite recursion detected !");
            }

            // Increase recursion depth (for logging purpose only)
            _Parent._RecursionDepth++;

            // Launch recursive check
            SPFResult R = _Parent.Check_Host(_Parent.ExpandDomainSpecForQuery(this._Arg));

            // Decrease recursion depth
            _Parent._RecursionDepth--;

            // RFC 4408 5.5.2
            switch (R) {
                case SPFResult.Pass:
                    _Parent._CheckedDomains[cleanDomain] = this._Qualifier;
                    return this._Qualifier;
                case SPFResult.Fail: case SPFResult.SoftFail: case SPFResult.Neutral:
                    _Parent._CheckedDomains[cleanDomain] = SPFMechanismQualifier.Neutral;
                    return SPFMechanismQualifier.Neutral;
                case SPFResult.TempError:
                    throw new TempErrorException(_Parent.Explanation);
                case SPFResult.PermError: case SPFResult.None:
                    throw new PermErrorException(_Parent.Explanation);
            }

            throw new PermErrorException("This line cannot be reached but raise compilation error if ommitted");
        }

    }

    public class A : SPFMechanismTerm {

        public A() : base() { this._MechanismType = SPFMechanismType.A; }

        public override SPFMechanismQualifier Check(SPFChecker _Parent, System.Net.IPAddress ip, string domain) {
            
            string DomainToQuery = string.IsNullOrEmpty(this._Arg) ? domain : this._Arg;

            // Test ip with A record of the specified domain
            return CheckFromRecord(ip, ip.AddressFamily == AddressFamily.InterNetworkV6 ? IP6Cidr : IP4Cidr, _Parent.ExpandDomainSpecForQuery(DomainToQuery), this._Qualifier, ip.AddressFamily == AddressFamily.InterNetworkV6 ? Heijden.DNS.Type.AAAA : Heijden.DNS.Type.A);

        }

    }

    public class MX : SPFMechanismTerm {

        public MX() : base() { this._MechanismType = SPFMechanismType.MX; }

        public override SPFMechanismQualifier Check(SPFChecker _Parent, System.Net.IPAddress ip, string domain) {
            string DomainToQuery = string.IsNullOrEmpty(this._Arg) ? domain : this._Arg;

            // Test ip with resolved adresses of MX record of the specified domain
            return CheckFromRecord(ip, ip.AddressFamily == AddressFamily.InterNetworkV6 ? IP6Cidr : IP4Cidr, _Parent.ExpandDomainSpecForQuery(DomainToQuery), this._Qualifier, Heijden.DNS.Type.MX);
        }

    }

    public class PTR : SPFMechanismTerm {

        public PTR() : base() { this._MechanismType = SPFMechanismType.PTR; }

        public override SPFMechanismQualifier Check(SPFChecker _Parent, System.Net.IPAddress ip, string domain) {
            
            string TargetName = string.IsNullOrEmpty(this._Arg) ? domain : this._Arg;

            // Execute PTR query for the ip
            string[] PTR_Result = null;
            try {
                PTR_Result = SPFChecker.DnsProxy.GetPTR(ip);
            }
            catch (SocketException ex) { return HandleSocketException(ex); }

            // Validate each hostname returned (see RFC for explanation)
            List<string> ValidatedNames = new List<string>();
            foreach (string HostName in PTR_Result) {
                IPAddress[] A_Result = null;
                try {
                    A_Result =
                        ip.AddressFamily == AddressFamily.InterNetworkV6 ?
                        SPFChecker.DnsProxy.GetAAAA(HostName) :
                        SPFChecker.DnsProxy.GetA(HostName);
                }
                catch (SocketException) {continue;}
                foreach (IPAddress Add in A_Result) {
                    if (Add.Equals(ip))
                        ValidatedNames.Add(HostName);
                }
            }

            // Test each validated hostname to be equal to the domain or a sub-domain of the domain
            foreach (string Name in ValidatedNames)
                if (Name.ToLower() == TargetName.ToLower() || Name.ToLower().EndsWith("." + TargetName.ToLower()))
                    return this._Qualifier;

            return SPFMechanismQualifier.Neutral;
        }

    }

    public class IP4 : SPFMechanismTerm {

        public IP4() : base() { this._MechanismType = SPFMechanismType.IP4; }

        public override SPFMechanismQualifier Check(SPFChecker _Parent, System.Net.IPAddress ip, string domain) {

            if (string.IsNullOrEmpty(this._Arg))
                throw new PermErrorException("ip4 argument cannot be empty.");

            string ip_to_compare_string = this._Arg;

            int Cidr = this.IP4Cidr;

            // Parse to IPAddress
            IPAddress TestedIP = null;
            try {
                TestedIP = IPAddress.Parse(ip_to_compare_string);
            }
            catch { throw new PermErrorException("Unable to parse ip"); }

            // If IPV6 Throw permerror
            if (TestedIP.AddressFamily != AddressFamily.InterNetwork)
                throw new PermErrorException("Given IP is not IPV4");

            // BitArray for tested ip
            BitArrayEx TestedBits = new BitArrayEx(TestedIP.GetAddressBytes());
            // If cidr apply mask
            TestedBits.ApplyMask(Cidr);

            // BitArray for ip to compare
            BitArrayEx BitsToCompare = new BitArrayEx(ip.GetAddressBytes());
            // If cidr apply mask
            BitsToCompare.ApplyMask(Cidr);

            // If equals, return qualifier
            if (TestedBits == BitsToCompare)
                return this._Qualifier;

            return SPFMechanismQualifier.Neutral;
        }

    }

    public class IP6 : SPFMechanismTerm {

        public IP6() : base() { this._MechanismType = SPFMechanismType.IP6; }

        public override SPFMechanismQualifier Check(SPFChecker _Parent, System.Net.IPAddress ip, string domain) {
            if (string.IsNullOrEmpty(this._Arg))
                throw new PermErrorException("ip6 argument cannot be empty.");

            string ip_to_compare_string = this._Arg;

            // Verify cidr
            int Cidr = this.IP6Cidr;

            // Parse to IPAddress
            IPAddress TestedIP = null;
            try {
                TestedIP = IPAddress.Parse(ip_to_compare_string);
            }
            catch { throw new PermErrorException("Unable to parse ip"); }
            
            // If IPV4 throw permError
            if (TestedIP.AddressFamily != AddressFamily.InterNetworkV6)
                throw new PermErrorException("Given IP is not IPV6");

            // BitArray for tested ip
            BitArrayEx TestedBits = new BitArrayEx(TestedIP.GetAddressBytes());
            // if cidr, apply mask
            TestedBits.ApplyMask(Cidr);

            // BitArray for ip to compare
            BitArrayEx BitsToCompare = new BitArrayEx(ip.GetAddressBytes());
            // if cidr, apply mask
            BitsToCompare.ApplyMask(Cidr);
            
            // If equals return qualifier
            if (TestedBits == BitsToCompare)
                return this._Qualifier;

            return SPFMechanismQualifier.Neutral;
        }

    }

    public class Exists : SPFMechanismTerm {

        public Exists() : base() { this._MechanismType = SPFMechanismType.Exists; }

        public override SPFMechanismQualifier Check(SPFChecker _Parent, System.Net.IPAddress ip, string domain) {

            if (string.IsNullOrEmpty(this._Arg))
                throw new PermErrorException("exists argument cannot be empty.");

            // Execute A query
            IPAddress[] result = null;
            try
            {
                result = SPFChecker.DnsProxy.GetA(_Parent.ExpandDomainSpecForQuery(this._Arg));
            }
            catch (SocketException ex) { HandleSocketException(ex); }

            // If record returned, return qualifier
            if (result != null && result.Length > 0)
                    return this._Qualifier;

            return SPFMechanismQualifier.Neutral;

        }

    }
}


